package com.ucode.springboot.starter.ESsearch.repository;

import java.io.IOException;

import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.HttpAsyncResponseConsumerFactory;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ucode.springboot.starter.ESsearch.annotation.metadata.ESDocumentData;
import com.ucode.springboot.starter.ESsearch.annotation.metadata.EsFieldData;
import com.ucode.springboot.starter.ESsearch.config.ElasticsearchConfig;
import com.ucode.springboot.starter.ESsearch.config.RestHighLevelClientHelper;
import com.ucode.springboot.starter.ESsearch.util.EsTools;

import cn.hutool.core.util.StrUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;

public abstract class BaseElasticsearchTemplate {
    
    protected static Log log = LogFactory.get(BaseElasticsearchTemplate.class);
    
    /**
     * 非分页，默认的查询条数
     */
    public static int DEFALT_PAGE_SIZE = 200;
    /**
     * 搜索建议默认条数
     */
    public static int COMPLETION_SUGGESTION_SIZE = 10;
    /**
     * 高亮字段默认tag
     */
    public static String HIGHLIGHT_TAG = "";
    /**
     * 创建索引mapping时，是否默认创建keyword
     */
    public static boolean DEFAULT_KEYWORDS = true;

    public static String DEFAULT_ES_HOST = "127.0.0.1:9200";
    /**
     * SCROLL查询 2小时
     */
    public static long DEFAULT_SCROLL_TIME = 2;
    /**
     * SCROLL查询 每页默认条数
     */
    public static int DEFAULT_SCROLL_PERPAGE = 100;
    /**
     * 默认百分比查询规格
     */
    public static double[] DEFAULT_PERCSEGMENT = {50.0,95.0,99.0};
    /**
     * 聚合查询返回最大条数
     */
    public static int AGG_RESULT_COUNT = Integer.MAX_VALUE;

    protected RestHighLevelClient restHighLevelClient;
    
    protected ElasticsearchConfig properties;
    
    protected static final RequestOptions COMMON_OPTIONS;

    public void setRestHighLevelClientHelper(RestHighLevelClientHelper restHighLevelClientHelper) {
        this.restHighLevelClient = restHighLevelClientHelper.builder();
    }
    
    public void setProperties(ElasticsearchConfig properties) {
        this.properties = properties;
    }

    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
        // 默认缓冲限制为100MB，此处修改为30MB。
        builder.setHttpAsyncResponseConsumerFactory(new HttpAsyncResponseConsumerFactory.HeapBufferedResponseConsumerFactory(30 * 1024 * 1024));
        COMMON_OPTIONS = builder.build();
    }
    
    /**
     * 构建IndexRequest
     * @author liliang
     * @date 2019年12月29日
     * @param indexName 索引名称
     * @param id 索引Id,为null则默认ES自动生成
     * @param object 索引文档对象
     * @return IndexRequest
     */
    protected static IndexRequest buildIndexRequest(String indexName, String id, Object object) {
        if(StrUtil.isNotBlank(id)){
            //.type("_doc")
            return new IndexRequest(indexName).id(id).source(JSON.toJSONString(object), XContentType.JSON);
        }else{
            return new IndexRequest(indexName).source(JSON.toJSONString(object), XContentType.JSON);
        }
    }
    
    /**
     * 构建UpdateRequest
     * @author liliang
     * @date 2019年12月31日
     * @param indexName 索引名称
     * @param id 索引主键
     * @param object 索引文档对象
     * @return
     */
    protected static UpdateRequest buildUpdateRequest(String indexName, String id, Object object) {
        return new UpdateRequest(indexName, id).doc(JSON.toJSONString(object), XContentType.JSON);
    }
    
    /**
     * 构建DeleteRequest
     * @author liliang
     * @date 2019年12月29日
     * @param indexName 索引名称
     * @param id 索引主键
     * @return IndexRequest
     */
    protected static DeleteRequest buildDeleteRequest(String indexName, String id) {
        return new DeleteRequest(indexName, id);
    }
    
    /**
     * 构建GetRequest
     * @author liliang
     * @date 2019年12月29日
     * @param indexName 索引名称
     * @param id 
     * @return GetRequest
     */
    protected static GetRequest buildGetRequest(String indexName, String id) {
        return new GetRequest(indexName, id);
    }
    
    /**
     * 构建SearchRequest
     * @author liliang
     * @date 2020年1月9日
     * @param queryBuilder 查询条件
     * @param indexNames 索引集合
     * @return
     */
    protected static SearchRequest buildSearchRequest(QueryBuilder queryBuilder,String... indexNames) {
        SearchRequest searchRequest = new SearchRequest(indexNames);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(queryBuilder);
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(DEFALT_PAGE_SIZE);
        searchRequest.source(searchSourceBuilder);
        
        if(log.isDebugEnabled()){
            log.debug(searchSourceBuilder.toString());
        }
        return searchRequest;
    }
    
    /**
     * search all
     *
     * @param index elasticsearch index name
     * @return {@link SearchResponse}
     * @author fxbin
     */
    protected SearchResponse search(String index) {
        SearchRequest searchRequest = new SearchRequest(index);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = null;
        try {
            searchResponse = restHighLevelClient.search(searchRequest, COMMON_OPTIONS);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return searchResponse;
    }
    
    /**
     * 构建CreateIndexRequest
     * 参考设置:https://blog.csdn.net/aa1215018028/article/details/103345143
     * @author liliang
     * @date 2020年1月9日
     * @param clazz 索引文档类类型
     * @return
     */
    public static CreateIndexRequest buildCreateIndexRequest(Class clazz){
        
        ESDocumentData metaData = EsTools.getDocEsIndexMetadata(clazz);
        String indexname = metaData.getIndexname();
        
        CreateIndexRequest request = new CreateIndexRequest(indexname);
        
        JSONObject properties  = buildMappingSource(new JSONObject(),clazz);
        boolean isNgram = properties.getBooleanValue("isNgram");
        if(isNgram){
            request.settings(Settings.builder()
                    .put("index.number_of_shards", metaData.getNumberOfShards())//索引的分片数.只能在创建索引时设置，不能修改
                    .put("index.number_of_replicas", metaData.getNumberOfReplicas())//索引的副本数
                    
                    /**
                     * 定义一个edge n-gram的token过滤器autocomplete_filter，并设置任何通过这个过滤器的词条，都会生成一个最小固定值为1，最大固定值为20的n-gram
                     */
                    .put("analysis.filter.autocomplete_filter.type","edge_ngram")//搜索推荐分词器类型->ngram分词
                    .put("analysis.filter.autocomplete_filter.min_gram",1)//推荐的分词最小长度
                    .put("analysis.filter.autocomplete_filter.max_gram",20)//推荐的分词最大长度
                    /**
                     * 自定义一个分析器autocomplete，并使用自定义的edge n-gram过滤器
                     */
                    .put("analysis.analyzer.autocomplete.type","custom")
                    .put("analysis.analyzer.autocomplete.tokenizer","standard")
                    .putList("analysis.analyzer.autocomplete.filter",new String[]{"lowercase","autocomplete_filter"})
            );
        }else{
            request.settings(Settings.builder()
                    .put("index.number_of_shards", metaData.getNumberOfShards())
                    .put("index.number_of_replicas", metaData.getNumberOfReplicas())
            );
        }
        
        //删除掉多余的参数
        properties.remove("isNgram");
        if(properties.size() > 0){
            JSONObject mapping = new JSONObject();
            mapping.put("properties", properties);
            
            request.mapping(mapping.toString(),XContentType.JSON);
        }
        
        return request;
    }
    
    
    /**
     * 构建 DeleteIndexRequest
     * @author liliang
     * @date 2020年1月9日
     * @param clazz 索引文档类类型
     * @return
     */
    protected static DeleteIndexRequest buildDeleteIndexRequest(Class clazz) {
        ESDocumentData metaData = EsTools.getDocEsIndexMetadata(clazz);
        String indexname = metaData.getIndexname();
        return new DeleteIndexRequest(indexname);
    }
    
    /**
     * 构建GetIndexRequest
     * @author liliang
     * @date 2020年1月9日
     * @param clazz 索引文档类类型
     * @return
     */
    protected static GetIndexRequest buildGetIndexRequest(Class clazz) {
        ESDocumentData metaData = EsTools.getDocEsIndexMetadata(clazz);
        String indexname = metaData.getIndexname();
        GetIndexRequest request = new GetIndexRequest(indexname);
        return request;
    }
    
    /**
     * 构建GetIndexRequest
     * @author liliang
     * @date 2020年1月9日
     * @param indexnames 索引名称数组
     * @return
     */
    protected static GetIndexRequest buildGetIndexRequest(String... indexnames) {
        GetIndexRequest request = new GetIndexRequest(indexnames);
        return request;
    }
    
    
    private static JSONObject buildMappingSource(JSONObject properties ,Class clazz){
        
        //因为有递归，所以先获取从递归带过来的，然后再删除，预防递归到深层时多带该参数，目的只想在最外层带回即可
        boolean isNgram = false;
        EsFieldData[] mappingDataList = EsTools.getDocEsMappingMetadatas(clazz);
        
        if(mappingDataList != null){
            for (int i = 0; i < mappingDataList.length; i++) {
                EsFieldData mappingData = mappingDataList[i];
                if(mappingData == null || mappingData.getFieldName() == null){
                    continue;
                }
                
                JSONObject propertie = new JSONObject();
                propertie.put("type", mappingData.getDatatype());
                if(!mappingData.getDatatype().equals("nested")){
                    /*************************************************************************/
                    if (mappingData.isNgram() &&
                            (mappingData.getDatatype().equals("text") || mappingData.getDatatype().equals("keyword"))) {
                        isNgram = true;
                    }
                    if(StrUtil.isNotBlank(mappingData.getCopyTo()))
                        propertie.put("copy_to", mappingData.getCopyTo());
                    if(StrUtil.isNotBlank(mappingData.getNullValue()))
                        propertie.put("null_value", mappingData.getNullValue());
                    if(!mappingData.isAllowSearch())
                        propertie.put("index", false);
                    
                    if (mappingData.isNgram() && mappingData.getDatatype().equals("text")) {
                        propertie.put("analyzer", "autocomplete");
                        propertie.put("search_analyzer", "standard");
                    }else if (mappingData.getDatatype().equals("text")) {
                        propertie.put("analyzer", mappingData.getAnalyzer());
                        propertie.put("search_analyzer", mappingData.getSearchAnalyzer());
                    }
                    
                    if (mappingData.isKeyword() && !mappingData.getDatatype().equals("keyword") && mappingData.isSuggest()) {
                        JSONObject field = new JSONObject();
                        JSONObject keyword = new JSONObject();
                        keyword.put("type", "keyword");
                        keyword.put("ignore_above", mappingData.getIgnoreAbove());
                        field.put("keyword", keyword);
                        JSONObject suggest = new JSONObject();
                        suggest.put("type", "completion");
                        suggest.put("analyzer", mappingData.getAnalyzer());
                        field.put("suggest", suggest);
                        
                        propertie.put("fields", field);
                    } else if (mappingData.isKeyword() && !mappingData.getDatatype().equals("keyword") && !mappingData.isSuggest()) {
                        JSONObject field = new JSONObject();
                        JSONObject keyword = new JSONObject();
                        keyword.put("type", "keyword");
                        keyword.put("ignore_above", mappingData.getIgnoreAbove());
                        field.put("keyword", keyword);
                        
                        propertie.put("fields", field);
                    }else if (!mappingData.isKeyword() && mappingData.isSuggest()) {
                        JSONObject field = new JSONObject();
                        JSONObject suggest = new JSONObject();
                        suggest.put("type", "completion");
                        suggest.put("analyzer", mappingData.getAnalyzer());
                        field.put("suggest", suggest);
                        
                        propertie.put("fields", field);
                    }
                    /*************************************************************************/
                    
                }else{
                    if(mappingData.getNestedClass() != null && mappingData.getNestedClass() != Object.class){
                        JSONObject subProperties = new JSONObject();
                        subProperties = buildMappingSource(subProperties,mappingData.getNestedClass()); 
                        
                        //因为有递归，所以先获取从递归带过来的，然后再删除，预防递归到深层时多带该参数，目的只想在最外层带回即可
                        isNgram = subProperties.getBooleanValue("isNgram");
                        subProperties.remove("isNgram");
                        
                        propertie.put("properties", subProperties);
                    }
                    
                }
                
                properties.put(mappingData.getFieldName(), propertie);
            }
        }
        
        //该值带出去,然后取出后删除
        properties.put("isNgram", isNgram);
        return properties;
    }
    
}
